﻿using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using JetBrains.Application;
using JetBrains.DataFlow;
using JetBrains.ProjectModel;
using GammaJul.ReSharper.GammaXaml.Caliburn;

namespace GammaJul.ReSharper.GammaXaml.Psi.Xaml {

	[SolutionComponent]
	public class CaliburnCache {
		private readonly Dictionary<IProjectModelElementPointer, Entry> _cache = new Dictionary<IProjectModelElementPointer,Entry>();
		private readonly Signal<IProject> _invalidated;
		private readonly ISolution _solution;
		private const string CaliburnMicroAssemblyName = "Caliburn.Micro";

		[NotNull]
		public Signal<IProject> Invalidated {
			get { return _invalidated; }
		}

		private sealed class Entry {
			internal readonly bool IsCaliburnMicroProject;

			internal Entry(bool isCaliburnMicroProject) {
				IsCaliburnMicroProject = isCaliburnMicroProject;
			}
		}

		private static bool ComputeIsCaliburnMicroProject([NotNull] IProject project) {
			CaliburnSupport support = project.GetConfiguredCaliburnSupport();
			switch (support) {
				case CaliburnSupport.Auto:
					return project.GetAssemblyReferences().Any(reference => reference.Name == CaliburnMicroAssemblyName);
				case CaliburnSupport.Yes:
					return true;
				case CaliburnSupport.No:
					return false;
				default:
					throw new ArgumentOutOfRangeException();
			}
		}

		public bool IsCaliburnMicroProject([CanBeNull] IProject project) {
			if (project == null)
				return false;

			IProjectModelElementPointer pointer = project.CreatePointer();
			lock (_cache) {
				Entry entry;
				if (!_cache.TryGetValue(pointer, out entry)) {
					entry = new Entry(ComputeIsCaliburnMicroProject(project));
					_cache.Add(pointer, entry);
				}
				return entry.IsCaliburnMicroProject;
			}
		}

		#region Invalidation

		private sealed class ChangeVisitor : RecursiveProjectModelChangeDeltaVisitor {
			private readonly CaliburnCache _owner;

			public override void VisitItemDelta(ProjectItemChange change) {
				base.VisitItemDelta(change);
				// Caliburn.Micro support has been changed in the project properties, invalidate
				if (change.IsPropertiesChanged && change.PropertyChangeSupport.IsPropertyChanged(typeof(CaliburnSupportProperty)))
					_owner.InvalidateCache(change.GetNewProject());
			}

			public override void VisitProjectReferenceDelta(ProjectReferenceChange projectReferenceChange) {
				base.VisitProjectReferenceDelta(projectReferenceChange);
				// A reference to Caliburn.Micro has been added or removed, invalidate
				if (projectReferenceChange.ProjectToModuleReference.Name == CaliburnMicroAssemblyName)
					_owner.InvalidateCache(projectReferenceChange.GetNewProject());
			}
			
			internal ChangeVisitor([NotNull] CaliburnCache owner) {
				_owner = owner;
			}

		}

		private void OnChange(ChangeEventArgs args) {
			var change = args.ChangeMap.GetChange<ProjectModelChange>(_solution);
			if (change != null)
				change.Accept(new ChangeVisitor(this));
		}

		private void InvalidateCache([CanBeNull] IProject project) {
			if (project == null)
				return;

			IProjectModelElementPointer pointer = project.CreatePointer();
			bool removed;
			lock (_cache)
				removed = _cache.Remove(pointer);

			if (removed)
				_invalidated.Fire(project);
		}

		#endregion


		public CaliburnCache(Lifetime lifetime, ISolution solution, ChangeManager changeManager) {
			_solution = solution;
			_invalidated = new Signal<IProject>(lifetime, "CaliburnCache.Invalidated");
			changeManager.Changed2.Advise(lifetime, OnChange);
		}

	}

}